#!/usr/bin/python2.4 -S
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Run unit tests for gvn.

Process a list of test filenames given on the command line, running each in
turn and reporting the results.  If no tests are listed, run all tests found
below the directory given in the GVN_RUNTEST_HOME environment variable, or
below the current directory if that variable is not set.  If the '--http'
option is given, the tests will be run using a local HTTP server rather than
using a repository in a local file directly.

"""

import imp
import optparse
import os
import re
import subprocess
import sys
import unittest

from errno import EEXIST

# Save home for the child runtests.
if 'GVN_RUNTEST_HOME' in os.environ:
  home = os.environ['GVN_RUNTEST_HOME']
else:
  home = os.environ['GVN_RUNTEST_HOME'] = os.getcwd()

sys.path.insert(0, home)
sys.path.insert(1, os.path.join(home, 'build/lib'))

import testing.coverage

import gvn
from gvn import platform
from testing import tmpdir

from testing import testcommon
from testing.httpd import Httpd
from testing.svnserve import Svnserve


def YieldTests(args):
  """Yield the filename and directory of each test in the list of args.

  Arguments:
    args -- A list of testfile paths.  If no list is given, all tests found
            below the 'home' directory (as defined above) will be yielded.

  Yields:
    A tuple consisting of a test name and the directory in which it was found.

  """

  if args:
    for test in args:
      (dirname, basename) = os.path.split(test)
      name = basename[:-3]
      yield (name, dirname)
    return

  for (root, dirs, filenames) in os.walk(home, topdown=True):
    if '.svn' in dirs:
      # Skip .svn directories.
      dirs.remove('.svn')
    if 'build' in dirs:
      # Skip distutils build directory.
      dirs.remove('build')
    if sys.platform == 'win32' and 'hooks' in dirs:
      # gvn.hooks is for unix only.
      dirs.remove('hooks')
    dirs.sort()

    filenames.sort()
    for filename in filenames:
      # TODO(ek): This will probably need to be broadened/generalized
      # beyond this.
      if not (filename.startswith('test_') and filename.endswith('.py')):
        continue
      if sys.platform == 'win32' and filename == 'test_userdb.py':
        # gvn.userdb is for unix only.
        continue
      name = filename[:-3]
      yield (name, root)


def Run(name, dir, http, svnserve, coverage, verbose):
  """Start a test by spawning a child instance of this same script.

  Since 'fork' doesn't exist on all platforms, we spawn children to run tests
  by creating another instance of this same script, but with the --child
  option (as well as other needed arguments).

  Arguments:
    name -- module name of the test to be run (i.e., filename with no
            extension)
    dir  -- absolute path to the directory in which the test is found
    coverage -- bool for coverage mode
    verbose  -- bool for verbose mode

  Returns:
    1 if the test had an error (returned status != 0), and reports the error
    0 otherwise

  """

  this_script = os.path.join(home, sys.argv[0])
  argv = [sys.executable, this_script, '--child']
  if http:
    argv.append('--http')
  elif svnserve:
    argv.append('--svnserve')
  if coverage:
    argv.append('--coverage')
  if verbose:
    argv.append('-v')
  argv.extend([name, dir, tmpdir.TmpDir()])
  status = subprocess.call(argv)

  if status == 0:
    return 0
  elif status < 0:
    sys.stderr.write("'%s' killed with signal %d\n" % (name, -status))
  else:
    sys.stderr.write("'%s' exited %d\n" % (name, status))
  sys.stderr.flush()
  return 1


def ChildRunTest(name, dir, tempdir, coverage, verbose):
  """Run an individual test file.

  Arguments:
    name    -- module name of the test (i.e., filename with no extension)
    dir     -- absolute path to the directory in which the test is found
    tempdir -- absolute path to an existing temp directory for use by the test
    coverage -- bool for coverage mode
    verbose -- bool for verbose mode

  Returns:
    0 if the test succeeeded
    1 otherwise

  """

  (fp, path, desc) = imp.find_module(name, [os.path.join(home, dir)])
  # TODO(epg): Don't, just do: os.environ = { ... }
  for i in 'EMAIL', 'GVN_EDITOR', 'SVN_EDITOR', 'VISUAL', 'EDITOR':
    try:
      del os.environ[i]
    except KeyError:
      pass

  tmpdir.SetTmpDir(tempdir)
  try:
    test = imp.load_module(name, fp, path, desc)
    loader = unittest.TestLoader()
    runner = unittest.TextTestRunner()
    if verbose:
      runner.verbosity = 2
      gvn.DiagLog = lambda f,*a: sys.stderr.write(f % a)

    if coverage:
      # Get cache path (testing/test_util => coverage/test_util).
      relative_dir = dir[len(os.path.join(home, 'testing')) + 1:]
      cache = os.path.join(home, 'coverage', relative_dir, name)
      try:
        os.makedirs(os.path.dirname(cache))
      except OSError, e:
        if e.errno != EEXIST:
          raise
      testing.coverage.the_coverage.cache = cache
      testing.coverage.erase()
      testing.coverage.start()

    if runner.run(loader.loadTestsFromModule(test)).wasSuccessful():
      result = 0
    else:
      result = 1

    if coverage:
        testing.coverage.stop()

  finally:
    fp.close()
  return result


def CoverageReport(opts, args):
  # TODO(epg): If no args, find all source (non-test) files.

  # Build map of Python files to coverage files.  Yeah, it's ugly.
  cpaths = {}
  for path in args:
    (dirname, basename) = os.path.split(path)
    if path.startswith('testing' + os.path.sep):
      dirname = re.sub('^testing', 'coverage', dirname)
    else:
      path = os.path.join('build', path)
      dirname = re.sub('^lib/gvn', 'coverage', dirname)
    basename = 'test_' + basename[:-3]
    cpaths[path] = os.path.join(dirname, basename)

  testing.coverage.report(cpaths, opts.coverage_missing)
  return 0

def main(opt_parse_tuple):
  """Parse options and run tests.

  When called without the '--child' option, set up the environment for running
  tests, create a temporary repository, build a list of tests, run each in
  turn, and report results.

  When called with the '--child' option, run a single test and return its
  result.  In this case the following command-line arguments are required,
  in the order listed:
    name   -- the name of the test to be run
    dir    -- the directory in which the test is found
    tmpdir -- the path to an existing temporary directory in which to create a
              test repository

  """

  (opts, args) = opt_parse_tuple

  if opts.coverage_report:
    return CoverageReport(opts, args)

  if opts.child:
    if opts.http or opts.svnserve:
      if opts.http:
        testcommon.RaDav('localhost', testing.httpd.GetPort())
      elif opts.svnserve:
        testcommon.RaSvn('localhost', testing.svnserve.GetPort())
      testcommon._username = 'unittest'
      testcommon._password = 'rayjandom'
    if len(args) != 3:
      exit(1)
    (name, dir, tempdir) = args
    testcommon.TestSetUp(tempdir)
    return ChildRunTest(name, dir, tempdir, opts.coverage, opts.verbose)

  testcommon.TestSetUp(tmpdir.TmpDir())
  testcommon.CopyTestData(os.path.join(home, 'testdata'),
                          os.path.join(tmpdir.TmpDir(), 'testdata'))

  if opts.http:
    server = Httpd(tmpdir.TmpDir(), opts.httpd)
    server.start()
  elif opts.svnserve:
    server = Svnserve(tmpdir.TmpDir(), opts.with_svnserve)
    server.start()
  else:
    server = None

  tests = failures = 0
  for (name, dir) in YieldTests(args):
    display = os.path.join(dir[len(home)+1:], name.replace('test_', ''))
    display = display.replace('testing' + os.sep, '')
    sys.stderr.write(display + ' ')
    sys.stderr.flush()

    tests += 1
    failures += Run(name, dir, opts.http, opts.svnserve, opts.coverage,
                    opts.verbose)

  if server is not None:
    server.stop()

  # Windows doesn't like removing the cwd.
  os.chdir(home)
  platform.rmtree(tmpdir.TmpDir())

  sys.stderr.write('%d failures / %d tests\n' % (failures, tests))
  sys.stderr.flush()
  return failures


if __name__ == '__main__':
  parser = optparse.OptionParser()
  parser.add_option('--http', action='store_true', dest='http', default=False,
      help='run tests over http (using neon)')
  parser.add_option('--svnserve', action='store_true', dest='svnserve',
                    default=False,
      help='run tests over svnserve')
  parser.add_option('--child', action='store_true', dest='child',
      default=False,
      help='run an individual test, called from a main runtest process')
  parser.add_option('--with-httpd', type='str', dest='httpd')
  parser.add_option('--with-svnserve', type='str')
  parser.add_option('--verbose', '-v', action='store_true')

  # coverage options
  parser.add_option('--coverage', action='store_true',
                    help='collect coverage data')
  parser.add_option('-r', '--coverage-report', action='store_true',
                    help='report on previously collected coverage data')
  parser.add_option('-m', '--coverage-missing', action='store_true',
                    help='show uncovered line numbers in --coverage-report')
  parser.set_defaults(coverage=False, coverage_missing=False,
                      coverage_report=False)

  sys.exit(main(parser.parse_args()))
